/*
 * Copyright 2001-2014 Viewstreet Technologies www.viewstreet.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.viewstreet.beanspice.processor;

import java.io.File;
import java.io.IOException;
import java.util.List;

import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.BodyDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.IAnnotationBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.Javadoc;
import org.eclipse.jdt.core.dom.MarkerAnnotation;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.TagElement;
import org.eclipse.jdt.core.dom.TextElement;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.text.edits.MalformedTreeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.viewstreet.beanspice.SpiceMethod;


/**
 * The Class BeanSpiceProcessor is responsible for generating code into the host of the BeanSpice Method see @SpiceMethod
 */
public class BeanSpiceProcessor {
	

	/** The src paths. */
	private final File [] srcPaths ;
	
	/** The src parser. */
	private SourceParser srcParser;
	
	/** The spice bean simple name. */
	private static String SPICE_BEAN_SIMPLE_NAME = SpiceMethod.class.getName().substring(SpiceMethod.class.getName().lastIndexOf('.')+1);
	
	/** The logger. */
	private static Logger logger = LoggerFactory.getLogger(BeanSpiceProcessor.class);
	

	
	/**
	 * The Constructor.
	 *
	 * @param srcPaths the src paths for the processor to locate host files usually the target/generated-sources
	 * @param classpath the classpath used during compilation of the sources
	 * @throws JavaModelException the java model exception
	 * @throws IOException the IO exception
	 */
	public BeanSpiceProcessor(File [] srcPaths,List<String> classpath) throws JavaModelException, IOException	{
		this.srcPaths = srcPaths;
		srcParser = new SourceParser(classpath);
	}
	
	
	/**
	 * Process.
	 *
	 * @param spiceDir the spice directory used to search for classes with @SpiceMethod annotations
	 * @return the number of @SpiceMethod methods processed
	 */
	public int process(File spiceDir)	{
		if(spiceDir == null || !spiceDir.exists())	{
			logger.warn("No Source Files to process "+spiceDir);
			return 0;
		}	else	{
			return processSpiceDirectory(spiceDir.listFiles());
		}
	}
	
	
	/**
	 * Process spice directory.
	 *
	 * @param spiceDir the spice dir
	 * @return the int
	 */
	private int processSpiceDirectory(File [] spiceDir)	{
		int methodsProcessed = 0;
		for (File file : spiceDir) {
	        if (file.isDirectory()) {
	        	methodsProcessed += processSpiceDirectory(file.listFiles()); // Calls same method again.
	        } else {
	        	logger.debug("Analyzing: " + file.getName());
	            try {
					CompilationUnit spiceClassCu = srcParser.runConversion(file,srcPaths);
					methodsProcessed += genHostCode(spiceClassCu);
				} catch (Exception e) {
					logger.error("Problems with "+file.getAbsolutePath(),e);
				}
	        }
	    }
		return methodsProcessed;
	}
	
	
	/**
	 * Gen host code.
	 *
	 * @param spiceClassCu the spice class cu
	 * @return the int
	 * @throws IOException the IO exception
	 * @throws MalformedTreeException the malformed tree exception
	 * @throws BadLocationException the bad location exception
	 */
	private int genHostCode(final CompilationUnit spiceClassCu) throws IOException, MalformedTreeException, BadLocationException	{
		int methodsProcessed = 0;
		if(spiceClassCu.types().size() < 1 || !(spiceClassCu.types().get(0) instanceof TypeDeclaration))	{
			return methodsProcessed;	// cannot be a spice bean
		}
		TypeDeclaration spiceType = (TypeDeclaration)spiceClassCu.types().get(0);
		ITypeBinding spicetypeBinding = null;
		for (MethodDeclaration spiceMethod : spiceType.getMethods()) {
			boolean needs_processing = false;
			try {
				List<?> mods = spiceMethod.modifiers();
				for (Object mod : mods) {
					if(mod instanceof MarkerAnnotation)	{
						MarkerAnnotation ma = (MarkerAnnotation)mod;
						if((ma.getTypeName().isSimpleName() && ma.getTypeName().toString().equals(SPICE_BEAN_SIMPLE_NAME)) || 
						   (ma.getTypeName().isQualifiedName() && ma.getTypeName().toString().equals(SpiceMethod.class.getName())))	{
							needs_processing = true;
							break;
						}
					}
				}
				if(!needs_processing)	{
					continue;
				}
				if(spicetypeBinding == null)	{	// Resolve Binding for class only if we are sure there is a Spicebean Annotation
					srcParser.checkCompilation(spiceClassCu, spiceType.getName().toString());    // Ensure the spice class has compiled properly
					spicetypeBinding = spiceType.resolveBinding();
				}
				
				MethodBean methodBean = new MethodBean(spicetypeBinding, spiceMethod);
				IAnnotationBinding[] annotations = methodBean.getMethodBinding().getAnnotations();
				for (IAnnotationBinding iAnnotationBinding : annotations) {
					if(iAnnotationBinding.getAnnotationType().getQualifiedName().equals(SpiceMethod.class.getCanonicalName()))	{
						methodsProcessed += processAnnotation(methodBean);
					} 
				}
			} catch (Exception e) {
				logger.error("Unable to resolve method source :"+spiceMethod.toString(),e);
				continue;
			}
		}
		return methodsProcessed;
	}
	
	
	
	/**
	 * Process annotation.
	 *
	 * @param methodBean the method bean
	 * @return the int
	 * @throws Exception the exception
	 * @throws MalformedTreeException the malformed tree exception
	 * @throws BadLocationException the bad location exception
	 */
	@SuppressWarnings("unchecked")
	private int processAnnotation(MethodBean methodBean) throws Exception, MalformedTreeException, BadLocationException	{
			logger.info("Found Spice Method "+ methodBean.toString() +" processing...");
			String spiceProperty = "m_"+methodBean.getTypeBinding().getQualifiedName().replace('.', '_');
			int methodsProcessed = 0;
			if(methodBean.getParameters().length >= 1)	{

				ITypeBinding hostTypeBinding = methodBean.getParameters()[0];				// First parameter is the hosting class always by convention
				
				EditableSource hostSrc = new EditableSource(srcParser, srcPaths, hostTypeBinding);
				
				TypeDeclaration tempTypeDec = genTypeDeclaration(methodBean, spiceProperty);
				
				MethodDeclaration new_methodDec = genMethod(hostSrc.getType(), tempTypeDec, methodBean);
				hostSrc.getType().bodyDeclarations().add(new_methodDec);
				if(!methodBean.isStatic())	{
					FieldDeclaration new_FieldDec = genField(hostSrc.getType(), tempTypeDec, spiceProperty);
					hostSrc.getType().bodyDeclarations().add(new_FieldDec);
				}	
				


				hostSrc.rewrite();
				methodsProcessed++;
			}
			return methodsProcessed;
		}	
	
	/**
	 * Gen field.
	 *
	 * @param hostType the host type
	 * @param tempTypeDec the temp type dec
	 * @param spiceProperty the spice property
	 * @return the field declaration
	 */
	private FieldDeclaration genField(TypeDeclaration hostType,TypeDeclaration tempTypeDec, String spiceProperty)	{
		FieldDeclaration new_FieldDec = (FieldDeclaration)ASTNode.copySubtree(hostType.getAST(), tempTypeDec.getFields()[0]);	// put into current ast
		// remove duplicate property
		for (FieldDeclaration existing_FieldDec : hostType.getFields()) {
			if(existing_FieldDec.fragments().get(0).toString().startsWith(spiceProperty))	{
				existing_FieldDec.delete();
			}
		}
		addJavaDoc(new_FieldDec,"BeanSpice property used for method delegation");
		return new_FieldDec;
	}
	
	
	/**
	 * Gen method.
	 *
	 * @param hostType the host type
	 * @param tempTypeDec the temp type dec
	 * @param methodBean the method bean
	 * @return the method declaration
	 */
	private MethodDeclaration genMethod(TypeDeclaration hostType, TypeDeclaration tempTypeDec, MethodBean methodBean)	{
		MethodDeclaration new_methodDec = (MethodDeclaration)ASTNode.copySubtree(hostType.getAST(), tempTypeDec.getMethods()[0]);	// put into current ast
		// remove any exiting methods with same params
		List<?> new_parms = new_methodDec.parameters();
		for (MethodDeclaration existing_MethodDec : hostType.getMethods()) {
			List<?> exiting_parms = existing_MethodDec.parameters();
			if(existing_MethodDec.getName().toString().equals(new_methodDec.getName().toString()) && new_parms.size() == exiting_parms.size()  )	{
				boolean fullMatch = true;
				for(int i=0;i<new_parms.size();i++)	{
					if(!new_parms.get(i).toString().equals(exiting_parms.get(i).toString())) {
						fullMatch = false;
						break;
					}
				}
				if(fullMatch)	{
					existing_MethodDec.delete();
				}
			}
		}

        StringBuilder spiceParameterString = new StringBuilder(); 
        for (int i = 0; i < methodBean.getParameters().length; i++) {
        	if(i >= 1)	{
        		spiceParameterString.append(',');
        	}
        	spiceParameterString.append(methodBean.getParameters()[i].getQualifiedName());
		}
		addJavaDoc(new_methodDec, "This is a BeanSpice Method : it is delegated to "+methodBean.getTypeBinding().getQualifiedName()+"."+methodBean.getMethodBinding().getName()+"("+spiceParameterString.toString()+")");
		return new_methodDec;
	
	}
	
	/**
	 * Gen type declaration.
	 *
	 * @param methodBean the method bean
	 * @param spiceProperty the spice property
	 * @return the type declaration
	 */
	private TypeDeclaration genTypeDeclaration(MethodBean methodBean, String spiceProperty)	{
		StringBuffer source = new StringBuffer();
        

		
		if(!methodBean.isStatic())	{
			// first the property
			source.append("protected");
			source.append(" ");
			source.append("transient");
			source.append(" ");
			source.append(methodBean.getTypeBinding().getQualifiedName());
			source.append(" ");
			source.append(spiceProperty);
			source.append(" = new ");
			source.append(methodBean.getTypeBinding().getQualifiedName());
			source.append("()");
			source.append(";\n");
		}
		
		// now the method
		Type rt = methodBean.getMethodDec().getReturnType2();

    	
		// Method Signature
		source.append("public ");
    	ITypeBinding rtBinding = rt.resolveBinding();
    	source.append(rtBinding.getQualifiedName());
    	source.append(" "+methodBean.getMethodDec().getName()+"(");
    	StringBuffer callArgs = new StringBuffer();
    	for (int i = 1; i < methodBean.getParameters().length; i++) {
        	if(i >= 2)	{
        		source.append(',');
        	}
        	source.append(methodBean.getParameters()[i].getQualifiedName());
        	source.append(" ");
        	source.append(methodBean.getParameterNames().get(i));
    		callArgs.append(","+methodBean.getParameterNames().get(i));
		}
		source.append(")    {\n");

		
		// Method Body
		if(rt.toString().equals("void"))	{
				source.append("     ");
			}
			else {
				source.append("     return ");
			}
		
			if(!methodBean.isStatic())	{
	        	source.append(spiceProperty+"."+methodBean.getMethodDec().getName());
			} else {
	        	source.append(methodBean.getTypeBinding().getQualifiedName()+"."+methodBean.getMethodDec().getName());
			}
        	source.append("(");
        	source.append("this");
    		source.append(callArgs);
        	source.append(");\n");
    	source.append("}");
		return srcParser.createType(source.toString());
	}
	
	
	
	/**
	 * Adds the java doc.
	 *
	 * @param node the node
	 * @param text the text
	 */
	@SuppressWarnings("unchecked")
	private void addJavaDoc(BodyDeclaration node,String text)	{
		Javadoc jDoc = node.getAST().newJavadoc();
        TextElement element = node.getAST().newTextElement();
        element.setText(text);
        TagElement tag = node.getAST().newTagElement();
        tag.fragments().add(element);
        jDoc.tags().add(tag);
		node.setJavadoc(jDoc);
	}
	
	
}	
